Published on

RAG检索增强生成(三)

Authors
  • avatar
    Name
    游戏人生
    Twitter

Chat History

ChatMessageHistory

chat history 是一组 Message 子类对象组成的列表,Message 子类可能是 HumanMessage、AIMessages。chat history 负责记录聊天历史。memory 模块负责将聊天记录的摘要或者最近几条聊天记录,转换为向量数据库中的向量,存储到向量数据库中。

创建一个 history 对象:

  import { ChatMessageHistory } from "langchain/stores/message/in_memory";
  import { HumanMessage, AIMessage } from "@langchain/core/messages";

  const history = new ChatMessageHistory();

添加消息:

  await history.addMessage(new HumanMessage("你好"));
  await history.addMessage(new AIMessage("你好啊!"));

获取历史记录

  const messages = await history.getMessages();

  console.log(messages);

输出内容如下:

  [
    HumanMessage {
      lc_serializable: true,
      lc_kwargs: { content: "你好", additional_kwargs: {}, response_metadata: {} },
      lc_namespace: [ "langchain_core", "messages" ],
      content: "你好",
      name: undefined,
      additional_kwargs: {},
      response_metadata: {}
    },
    AIMessage {
      lc_serializable: true,
      lc_kwargs: { content: "你好啊!", additional_kwargs: {}, response_metadata: {} },
      lc_namespace: [ "langchain_core", "messages" ],
      content: "你好啊!",
      name: undefined,
      additional_kwargs: {},
      response_metadata: {}
    }
  ]

手动维护 chat history

  import { ChatPromptTemplate, MessagesPlaceholder } from "@langchain/core/prompts";
  import { ChatAlibabaTongyi } from "@langchain/community/chat_models/alibaba_tongyi";

  const chatModel = new ChatAlibabaTongyi({
    model: "qwen-turbo", // Available models: qwen-turbo, qwen-plus, qwen-max
    temperature: 1,
  });

  const prompt = ChatPromptTemplate.fromMessages([
      ["system", `你是一个乐于助人的助手,尽你所能地回答所有问题。
      你很健谈,能从上下文中提供大量具体细节。
      如果你不知道问题的答案,那就如实地说你不知道。`],
      new MessagesPlaceholder("history_message"),
  ]);

  const chain = prompt.pipe(chatModel);

其中 MessagesPlaceholder 创建一个名为 history_message 的插槽,chain 中对应的参数将会替换这部分。

创建一个 chat history,然后向其中添加一条历史记录:

  import { ChatMessageHistory } from "langchain/stores/message/in_memory";
  import { HumanMessage, AIMessage } from "@langchain/core/messages";

  const history = new ChatMessageHistory();
  await history.addMessage(new HumanMessage("你好,我是叮当猫"));

  const res1 = await chain.invoke({
    history_message: await history.getMessages()
  });

  console.log(res1);

返回内容如下:

  AIMessage {
    lc_serializable: true,
    lc_kwargs: {
      content: "你好,叮当猫!很高兴见到你。如果你有任何问题或者需要帮助,请随时告诉我。",
      tool_calls: [],
      invalid_tool_calls: [],
      additional_kwargs: {},
      response_metadata: {}
    },
    lc_namespace: [ "langchain_core", "messages" ],
    content: "你好,叮当猫!很高兴见到你。如果你有任何问题或者需要帮助,请随时告诉我。",
    name: undefined,
    additional_kwargs: {},
    response_metadata: {
      tokenUsage: { promptTokens: 64, completionTokens: 20, totalTokens: 84 }
    },
    tool_calls: [],
    invalid_tool_calls: []
  }

添加一条人类新提问:

  await history.addMessage(res1)
  await history.addMessage(new HumanMessage("我是谁"));

获取回答,传入history

  const res2 = await chain.invoke({
    history_message: await history.getMessages(),
  });
  console.log(res2);

返回内容如下:

  AIMessage {
    lc_serializable: true,
    lc_kwargs: {
      content: "你自称是叮当猫,这通常指的是卡通角色《蓝皮鼠和大脸猫》中的角色,或者在一些衍生作品中,也可能指代哆啦A梦(Doraemon)系列里的叮当猫(Ding Dong Bell)。如果你在玩某个角色扮演游戏或"... 20 more characters,
      tool_calls: [],
      invalid_tool_calls: [],
      additional_kwargs: {},
      response_metadata: {}
    },
    lc_namespace: [ "langchain_core", "messages" ],
    content: "你自称是叮当猫,这通常指的是卡通角色《蓝皮鼠和大脸猫》中的角色,或者在一些衍生作品中,也可能指代哆啦A梦(Doraemon)系列里的叮当猫(Ding Dong Bell)。如果你在玩某个角色扮演游戏或"... 20 more characters,
    name: undefined,
    additional_kwargs: {},
    response_metadata: {
      tokenUsage: { promptTokens: 96, completionTokens: 73, totalTokens: 169 }
    },
    tool_calls: [],
    invalid_tool_calls: []
  }

自动维护 chat history

使用 RunnableWithMessageHistory 给任意 chain 包裹一层,就能添加聊天记录管理的能力。

  import { RunnableWithMessageHistory } from "@langchain/core/runnables";

  const chatModel = new ChatAlibabaTongyi({
    model: "qwen-turbo", // Available models: qwen-turbo, qwen-plus, qwen-max
    temperature: 1,
  });

  const prompt = ChatPromptTemplate.fromMessages([
    ["system", "你是一个乐于助人的助手,尽你所能地回答所有问题。"],
    new MessagesPlaceholder("history_message"),
    ["human","{input}"]
  ]);

  const history = new ChatMessageHistory();
  const chain = prompt.pipe(chatModel)

  const chainWithHistory = new RunnableWithMessageHistory({
    runnable: chain,
    getMessageHistory: (_sessionId) => history,
    inputMessagesKey: "input",
    historyMessagesKey: "history_message",
  });

RunnableWithMessageHistory 参数含义:

  • runnable: 需要被包裹的 chain,可以是任意 chain
  • getMessageHistory: 接收一个函数,函数需要根据传入的 _sessionId,去获取对应的 ChatMessageHistory 对象
  • inputMessagesKey:用户传入的信息 key 的名称,因为 RunnableWithMessageHistory 要自动记录用户和 llm 发送的信息,所以需要在这里声明用户传入信息的 key
  • historyMessagesKey: 聊天记录在 prompt 中的 key,因为要自动的把聊天记录注入到 prompt 中
  • outputMessagesKey: 只有一个输出就省略,如果有多个输出需要指定哪个是 llm 的回复,也就是需要存储的信息

直接调用这个 chain,历史记录会自动保存,这里除了正常 invoke 传入的参数外,还需要指定当前对话的 sessionId。

  const res1 = await chainWithHistory.invoke({
    input: "你好,我是叮当猫",
  },{
      configurable: { sessionId: "none" }
  });
  console.log(res1);

再次提问:

  const res2 = await chainWithHistory.invoke({
    input: "我是谁",
  },{
      configurable: { sessionId: "none" }
  });
  console.log(res2);

返回内容如下:

  AIMessage {
    lc_serializable: true,
    lc_kwargs: {
      content: "你自称是叮当猫,这通常是指卡通角色《蓝猫淘气三千问》中的主角之一,或者指《哆啦A梦》(Doraemon)中的重要角色,哆啦A梦的神奇小口袋里的道具“叮当”(铜锣)。如果你在玩角色扮演游戏或者有其他特定"... 36 more characters,
      tool_calls: [],
      invalid_tool_calls: [],
      additional_kwargs: {},
      response_metadata: {}
    },
    lc_namespace: [ "langchain_core", "messages" ],
    content: "你自称是叮当猫,这通常是指卡通角色《蓝猫淘气三千问》中的主角之一,或者指《哆啦A梦》(Doraemon)中的重要角色,哆啦A梦的神奇小口袋里的道具“叮当”(铜锣)。如果你在玩角色扮演游戏或者有其他特定"... 36 more characters,
    name: undefined,
    additional_kwargs: {},
    response_metadata: {
      tokenUsage: { promptTokens: 72, completionTokens: 90, totalTokens: 162 }
    },
    tool_calls: [],
    invalid_tool_calls: []
  }

自动生成 chat history 摘要

实现一个自动对当前聊天历史记录进行总结,然后让 llm 根据总结的信息回复用户的 chain。

首先实现一个总结 chain,这个 chain 接受两个参数:

  • summary,上一次总结的信息
  • new_lines,用户和 llm 新的回复

返回值是一个纯文本的信息,根据历史的 summary 信息和用户新的对话生成的新 summary。

  import { ChatAlibabaTongyi } from "@langchain/community/chat_models/alibaba_tongyi";
  import { ChatPromptTemplate } from "@langchain/core/prompts";
  import { RunnableSequence } from "@langchain/core/runnables";
  import { StringOutputParser } from "@langchain/core/output_parsers";

  const summaryModel = new ChatAlibabaTongyi({
    model: "qwen-turbo", // Available models: qwen-turbo, qwen-plus, qwen-max
    temperature: 1,
  });

  const summaryPrompt = ChatPromptTemplate.fromTemplate(`
  逐步总结所提供的对话内容,在之前的总结基础上添加新的总结

  当前摘要:
  {summary}

  新的对话方式:
  {new_lines}

  新摘要:
  `); 

  const summaryChain = RunnableSequence.from([
    summaryPrompt,
    summaryModel,
    new StringOutputParser(),
  ]);

调用:

  const newSummary = await summaryChain.invoke({
    summary: "",
    new_lines: "我是叮当猫"
  });

再次调用,并传入上次的 summary

  await summaryChain.invoke({
    summary: newSummary,
    new_lines: "我会飞"
  });

输出内容如下:

  '在先前的对话总结中,我们主要探讨了整体情况和可能的话题范围。随着新角色"我是叮当猫"的加入,对话可能转向与超自然能力、冒险、童话世界或者科幻元素相关的内容,特别是如果"我会飞"的能力成为讨论的核心。然'... 40 more characters

如此就实现了一个渐进式总结历史聊天记录的 chat bot,然后以此为基础构建一个 chat bot,其会自动将聊天记录进行 summary,并且传递给 llm 作为上下文。

首先创建一个基础的 prompt 模板,和用于存储聊天记录的 ChatMessageHistory,并且创建一个 summary 字符串,用来存储用户之前聊天记录的总结信息。

  import { ChatMessageHistory } from "langchain/stores/message/in_memory";

  const chatModel = new ChatAlibabaTongyi({
    model: "qwen-turbo", // Available models: qwen-turbo, qwen-plus, qwen-max
    temperature: 1,
  });

  const chatPrompt = ChatPromptTemplate.fromMessages([
      ["system", `你是一个乐于助人的助手,尽你所能地回答所有问题。

      以下是聊天记录摘要:
      {history_summary}
      `],
      ["human","{input}"]
  ]);

  let summary = ""
  const history = new ChatMessageHistory();

实现完整的 chain:

  import { RunnablePassthrough } from "@langchain/core/runnables";

  const chatChain = RunnableSequence.from([
    {
      input: new RunnablePassthrough({
        func: (input) => history.addUserMessage(input)
      })
    },
    RunnablePassthrough.assign({
      history_summary: () => summary
    }),
    chatPrompt,
    chatModel,
    new StringOutputParser(),
    new RunnablePassthrough({
      func: async (input) => {
        history.addAIChatMessage(input);
        const messages = await history.getMessages();
        const new_lines = getBufferString(messages);
        const newSummary = await summaryChain.invoke({
            summary,
            new_lines
        });
        history.clear();
        summary = newSummary;   
      }
    })
]);

RunnableMap 并行的执行多个 runnable 对象,然后返回结果对象。

  import { RunnableMap } from "@langchain/core/runnables"

  const mapChain = RunnableMap.from({
      a: () => "a",
      b: () => "b"
  })

  const res = await mapChain.invoke()
  // { a: "a", b: "b" }

函数也是一种 runnable 对象,这两个函数是并行执行的。这两个函数换成任意 runnable 对象,例如两个 chain 即会并行执行这两个 chain,并且返回相应的结果。

在 RunnableSequence 的数组中,如果有 object 类型的值,会被自动转换成 RunnableMap,也就是 chain 中第一个 object 对象本质上是新建了一个 RunnableMap。

RunnablePassthrough 是一个特殊的 runnable,它不会执行任何操作,而是直接返回输入。可以理解是 runnable chain 中的特殊节点。

使用代码①

  new RunnablePassthrough({func: (input)=> void})

的目的:

  • 如果只写 new RunnablePassthrough(),就是把用户输入的 input 再传递到下一个 runnable 节点中,不做任何操作。因为 RunnableMap 返回值是对其中每个 chain 的执行,然后将返回值作为结果传递给下一个 runnable 节点,如果不对 input 使用 RunnablePassthrough 则下个节点就拿不到 input 的值
  • ① 中的 func 函数是在传递 input 的过程中,执行一个函数,这个函数返回值是 void,即无论其内容是什么,都不会对 input 造成影响。

测试 chain:

  const res1 = await chatChain.invoke("我现在饿了");
  console.log(res1);

输出内容如下:

  如果你饿了,可以考虑吃点东西。你可以选择制作简单的三明治、热汤或者是一碗方便面,如果你在家;外出的话,可以去附近的餐馆、便利店或者点外卖。别忘了喝点水哦。

继续交流:

  const res2 = await chatChain.invoke("我今天想吃方便面");
  console.log(res2);

返回内容如下:

  当然可以!如果你想要吃方便面,这里有一些建议:

  1. **选择口味**:确保挑选你喜欢的口味,比如经典的红烧牛肉、豚骨、泡椒凤爪或者蔬菜味等。
  2. **准备材料**:根据包装上的指示,准备好所需的配料,如调料包、蔬菜、鸡蛋(如果需要)等。
  3. **烧水煮面**:烧一锅开水,按照方便面包装上的时间煮面,通常大约3-5分钟。
  4. **添加配料**:煮好面后,把调料包、蔬菜或其他喜欢的配料加入,搅拌均匀。
  5. **享受美食**:你的快速方便面就完成了,记得配一碗热水或茶,补充一下水分。
  6. **健康小贴士**:虽然方便面快捷,但为了均衡营养,偶尔可以搭配一些蔬菜、水果或低脂酸奶来提升饱腹感和营养。

  享用你的方便面时,别忘了适时休息,补充能量哦!

Memory

内置 Memory 的机制

BufferWindowMemory

对聊天记录加了一个滑动窗口,只会记忆 k 个对话

  import { BufferWindowMemory } from "langchain/memory";
  import { ConversationChain } from "langchain/chains";
  import { ChatOpenAI } from "@/langchain/openai";

  const model = new ChatOpenAI();
  const memory = new BufferWindowMemory({ k: 1 });
  const chain = new ConversationChain({ llm: model, memory: memory });

ConversationSummaryMemory

对聊天记录进行 summary,并保存在 memory 中,每次调用 chain 时,都会将 summary 作为 prompt 的一部分,即随着聊天不断生成和更新聊天记录摘要。

  import { ConversationSummaryMemory } from "langchain/memory";
  import { PromptTemplate } from "@langchain/core/prompts";

  const memory = new ConversationSummaryMemory({
      memoryKey: "summary",
      llm: new ChatOpenAI({
        verbose: true, // 开启 verbose 模式查看内部执行
      }),
    });

  const model = new ChatOpenAI();
  const prompt = PromptTemplate.fromTemplate(`
    你是一个乐于助人的助手,尽你所能回答所有问题。

    这是聊天记录的摘要:
    {summary}
    Human: {input}
    AI:`);
  const chain = new ConversationChain({ llm: model, prompt, memory, verbose: true });

  const res1 = await chain.call({ input: "我是小明" });
  const res2 = await chain.call({ input: "我叫什么?" });

将 BufferWindowMemory 和 ConversationSummaryMemory 结合起来,根据 token 数量,如果上下文历史过大时就切换到 summary,如果上下文比较小时就使用原始的聊天记录,也就成了 ConversationSummaryBufferMemory。

  import { ChatOpenAI } from "@langchain/openai";
  import { ConversationSummaryBufferMemory } from "langchain/memory";
  import { ConversationChain } from "langchain/chains";

  const model = new ChatOpenAI();
  const memory = new ConversationSummaryBufferMemory({
    llm: new ChatOpenAI(),
    maxTokenLimit: 200
  });

  const chain = new ConversationChain({ llm: model, memory: memory, verbose: true });

原理跟前面的两个 memory 的机制类似,会计算当前完整聊天记录的 token 数,判断是否超过设置的 maxTokenLimit,如果超过则对聊天记录总结成 summary 输入进去。

ConversationSummaryBufferMemory 的思想,短聊天使用 BufferWindowMemory、长聊天就成为 ConversationSummaryMemory。

EntityMemory

EntityMemory 是一个特殊的 Memory,它将实体(实体是任何类型的对象)存储在 memory 中,并使用 LLM 生成实体的描述。EntityMemory 希望模拟的是在聊天中去生成和更新不同的实体的描述。

  import { ChatOpenAI } from "@langchain/openai";
  import { EntityMemory, ENTITY_MEMORY_CONVERSATION_TEMPLATE } from "langchain/memory";
  import { ConversationChain } from "langchain/chains";

  const model = new ChatOpenAI();
  const memory = new EntityMemory({
      llm: new ChatOpenAI({
          verbose: true, // 开启 verbose 模式查看内部执行
      }),
      chatHistoryKey: "history",
      entitiesKey: "entities"
  });

  const chain = new ConversationChain({ 
      llm: model, 
      prompt: ENTITY_MEMORY_CONVERSATION_TEMPLATE,
      memory: memory, 
      verbose: true 
  });

ENTITY_MEMORY_CONVERSATION_TEMPLATE 是 langchain 提供的用于 EntityMemory chat 的 默认的 prompt。也可以自定义 prompt。

进行对话:

  const res1 = await chain.call({ input: "Hello, 我是小明!" });
  const res2 = await chain.call({ input: "Dcat 是叮当猫,会跑会飞。" });

在 LCEL 中集成 memory

BaseMemory 是一个抽象类,不能直接使用,需要使用它的子类。所有 memory 都需要继承该接口。核心就是就是两个方法:

  • loadMemoryVariables: 返回当前记忆的内容,如果有些记忆是依赖于输入的,例如 EntityMemory,就需要传入一些输入,让 memory 返回对应输入的记忆。 在 EntityMemory 的场景下,就是 memory 需要根据传入的信息提取输入中的实体,并且返回实体中相关的记忆
  • saveContext: 保存当前上下文,包括输入和输出,以便下次调用。

以 BufferMemory 在 LCEL 中使用 memory。

  import { BufferMemory } from "langchain/memory";
  import { ChatAlibabaTongyi } from "@langchain/community/chat_models/alibaba_tongyi";

  const chatModel = new ChatAlibabaTongyi({
      model: "qwen-turbo", // Available models: qwen-turbo, qwen-plus, qwen-max
      temperature: 1,
      verbose:true
  });

  const memory = new BufferMemory();

  const TEMPLATE = `
  你是一个乐于助人的 ai 助手,尽你所能回答所有问题。

  这是跟人类沟通的聊天历史:
  {history}

  据此回答人类的问题:
  {input}
  `
  const prompt = ChatPromptTemplate.fromTemplate(TEMPLATE);

构建 chain:

  import { RunnableSequence, RunnablePassthrough } from "@langchain/core/runnables";
  import { StringOutputParser } from "@langchain/core/output_parsers";

  let tempInput = ""

  const chain =  RunnableSequence.from([
    {
      input: new RunnablePassthrough(),
      memoryObject: async (input) => {
        const history = await memory.loadMemoryVariables({
          input,
        });
        tempInput = input;
        return history;
      }
    },

    RunnablePassthrough.assign({
      history: (input) => input.memoryObject.history
    }),

    prompt,
    chatModel,
    new StringOutputParser(),

    new RunnablePassthrough({
      func: async (output) => {
        await memory.saveContext({
          input: tempInput,
        }, {
          output,
        })
      }
    }),
  ]);

tempInput 用于临时存储用户的 input 输入。

首先使用 new RunnablePassthrough() 对 input 进行透传,然后创建一个函数(Runnable 对象),以用户的 input 作为输入,使用 loadMemoryVariables 去加载 memory 中的数据,并将用户的输入保存到 tempInput。

加载出来的 history 对象是一个 object,其是 memory 根据用户这次 input 返回的数据,如果是 BufferMemory,则里面只有一个 history key。其他的 memory 根据不同的类型,返回的数据也是不一样的,例如 EntityMemory 就会有 history 和 entities 两个 key。

在 chain 的第二个节点,使用 RunnablePassthrough.assign() 提取出 memoryObject 中的 history 值。 assign 会在上一个节点输入的基础上,再添加新的数据。 这样后面的 prompt 就接收到了 history 和 input 两个输入值。

prompt、chatModel、new StringOutputParser() 就是基础的使用 prompt 激发 llm 输出内容,然后使用 StringOutputParser 去提取出纯文本的内容。

最后一个节点,使用 RunnablePassthrough 去执行一个函数,将用户的输入和输出使用 saveContext 存储到 memory 中。

使用下面这个技巧去打印 chain 中间传递的内容,了解数据在 chain 中是如何流动的:

  [
    ...
    prompt,
    new RunnablePassthrough({
      func: (input) => console.log(input)
    }),
    chatModel,
    ...
  ]

实现自定义 chat history

通过查看 BaseListChatMessageHistory 源码 可知真正需要实现的就是addUserMessage、addAIChatMessage、addAIMessage、clear 几个方法:

  • getMessages:获取存储在 history 中所有聊天记录
  • addMessage:添加单条 message
  • addMessages:添加 message 数组
  • clear:清空聊天记录

完整代码如下:

  import { BaseListChatMessageHistory } from "@langchain/core/chat_history";
  import {
    BaseMessage,
    StoredMessage,
    mapChatMessagesToStoredMessages,
    mapStoredMessagesToChatMessages,
  } from "@langchain/core/messages";
  import fs from "node:fs";
  import path from "node:path";

  export interface JSONChatHistoryInput {
    sessionId: string; // sessionId 是区别于不同对话的 id,在工程中一般使用 uuid
    dir: string; // dir 是存储聊天记录 json 文件的目录
  }

  export class JSONChatHistory extends BaseListChatMessageHistory {

    /**
     * BaseListChatMessageHistory 继承了 Serializable,声明 lc_namespace 是方便 
     * langchain 在序列化和反序列化时,找到 json 中对象对应的内置类。例如当把 message 序列化,
     * 再反序列化后,打印出来依旧是对应 langchain 内部的类的实例化对象,依靠的就是这个。
     */
    lc_namespace = ["langchain", "stores", "message"];

    sessionId: string;
    dir: string;

    constructor(fields: JSONChatHistoryInput) {
      super(fields);
      this.sessionId = fields.sessionId;
      this.dir = fields.dir;
    }

    /**
     * 从对应的文件中读取 json 内容,然后使用 mapStoredMessagesToChatMessages 序列化成对应的 message 对象
     */
    async getMessages(): Promise<BaseMessage[]> {
      const filePath = path.join(this.dir, `${this.sessionId}.json`);

      try {
        if (!fs.existsSync(filePath)) {
          this.saveMessagesToFile([]);
          return [];
        }

        const data = fs.readFileSync(filePath, { encoding: "utf-8" });
        const storedMessages = JSON.parse(data) as StoredMessage[];

        return mapStoredMessagesToChatMessages(storedMessages);
      } catch (error) {
        console.error(`Failed to read chat history from ${filePath}`, error);
        return [];
      }
    }

    async addMessage(message: BaseMessage): Promise<void> {
      const messages = await this.getMessages();
      messages.push(message);
      await this.saveMessagesToFile(messages);
    }

    async addMessages(messages: BaseMessage[]): Promise<void> {
      const existingMessages = await this.getMessages();
      const allMessages = existingMessages.concat(messages);
      await this.saveMessagesToFile(allMessages);
    }

    async clear(): Promise<void> {
      const filePath = path.join(this.dir, `${this.sessionId}.json`);
      try {
        fs.unlinkSync(filePath);
      } catch (error) {
        console.error(`Failed to clear chat history from ${filePath}`, error);
      }
    }

    /**
     * 使用 mapChatMessagesToStoredMessages 去对 messages 进行序列化,然后写文件到 json 文件中
     * @param messages 
     */
    private async saveMessagesToFile(messages: BaseMessage[]): Promise<void> {
      const filePath = path.join(this.dir, `${this.sessionId}.json`);
      const serializedMessages = mapChatMessagesToStoredMessages(messages);

      try {
        fs.writeFileSync(filePath, JSON.stringify(serializedMessages, null, 2), {
          encoding: "utf-8",
        });
      } catch (error) {
        console.error(`Failed to save chat history to ${filePath}`, error);
      }
    }
  }

测试:

  import { JSONChatHistory } from "./JSONChatHistory/index.ts"
  import { AIMessage, HumanMessage } from "@langchain/core/messages";

  const history = new JSONChatHistory({
    dir: "chat_data",
    sessionId: "test"
  })

  await history.addMessages([
    new HumanMessage("你好,我是叮当猫"),
    new AIMessage("你好"),
  ]);

  const messages = await history.getMessages();
  console.log(messages);

输出内容如下:

  [
    HumanMessage {
      lc_serializable: true,
      lc_kwargs: {
        content: "你好,我是叮当猫",
        additional_kwargs: {},
        response_metadata: {}
      },
      lc_namespace: [ "langchain_core", "messages" ],
      content: "你好,我是叮当猫",
      name: undefined,
      additional_kwargs: {},
      response_metadata: {}
    },
    AIMessage {
      lc_serializable: true,
      lc_kwargs: { content: "你好", additional_kwargs: {}, response_metadata: {} },
      lc_namespace: [ "langchain_core", "messages" ],
      content: "你好",
      name: undefined,
      additional_kwargs: {},
      response_metadata: {}
    }
  ]

应用在 memory 中:

  import { ChatAlibabaTongyi } from "@langchain/community/chat_models/alibaba_tongyi";
  import { BufferMemory } from "langchain/memory";
  import { ConversationChain } from "langchain/chains";


  const chatModel = new ChatAlibabaTongyi({
    model: "qwen-turbo", // Available models: qwen-turbo, qwen-plus, qwen-max
    temperature: 1,
  });
  const memory = new BufferMemory({
    chatHistory: history
  });
  const chain = new ConversationChain({ llm: chatModel, memory: memory });
  const res1 = await chain.call({ input: "我是谁?" });
  console.log(res1);

输出内容如下:

  {
    response: "你是在说中文的“叮当猫”,这通常是中国动画《哆啦A梦》中的主角,一个来自22世纪的机器猫,以你的名字“Ding Dang”进行交流。"
  }

查看 chat_data 目录下的文件,可以发现,保存了聊天记录。记录如下:

  [
    {
      "type": "human",
      "data": {
        "content": "你好,我是叮当猫",
        "additional_kwargs": {},
        "response_metadata": {}
      }
    },
    {
      "type": "ai",
      "data": {
        "content": "你好",
        "additional_kwargs": {},
        "response_metadata": {}
      }
    },
    {
      "type": "human",
      "data": {
        "content": "我是谁?",
        "additional_kwargs": {},
        "response_metadata": {}
      }
    },
    {
      "type": "ai",
      "data": {
        "content": "你是在说中文的“叮当猫”,这通常是中国动画《哆啦A梦》中的主角,一个来自22世纪的机器猫,以你的名字“Ding Dang”进行交流。",
        "additional_kwargs": {},
        "response_metadata": {}
      }
    }
  ]

至此,通过自定义 chat history 实现把历史记录存储到本地文件和从本地文件中读取到 chat history 中,并且将自定义的 chat history 作为 memory 内置使用的 chatHistory 成功的将 memory 接入了存储中。